<template>
  <table class="drop-table" ref="tableRef" @click="onTableClick">
    <thead class="drop-table-head">
      <tr>
        <th class="col-handle" v-if="!disableDrag"></th>
        <th
          v-for="(col, colIndex) in columns"
          :key="colIndex"
          :style="columnStyles[col.dataIndex]"
        >
          <template v-if="col.type && col.type == 'checkbox'">
            <input
              type="checkbox"
              v-model="allChecked"
              @click="checkAll"
              v-if="multiple"
            />
          </template>

          <template v-else>
            {{ col.title }}
          </template>
        </th>
      </tr>
    </thead>
    <tbody class="drop-table-body">
      <tr
        v-for="(item, index) in dataSource"
        :key="item[keyField]"
        :data-key="item[keyField]"
        :class="item.__selected ? 'selected' : ''"
      >
        <td class="col-handle" v-if="!disableDrag">
          <svg
            class="icon sortable-handle"
            v-if="item.__selected ? false : true"
            t="1662622355055"
            viewBox="0 0 1024 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            p-id="5663"
            width="1em"
            height="1em"
          >
            <path
              d="M384 768C348.653776 768 320 796.653776 320 832 320 867.346224 348.653776 896 384 896 419.346224 896 448 867.346224 448 832 448 796.653776 419.346224 768 384 768ZM384 448C348.653776 448 320 476.653776 320 512 320 547.346224 348.653776 576 384 576 419.346224 576 448 547.346224 448 512 448 476.653776 419.346224 448 384 448ZM384 128C348.653776 128 320 156.653779 320 192 320 227.346221 348.653776 256 384 256 419.346224 256 448 227.346221 448 192 448 156.653779 419.346224 128 384 128ZM640 768C604.653776 768 576 796.653776 576 832 576 867.346224 604.653776 896 640 896 675.346221 896 704 867.346224 704 832 704 796.653776 675.346221 768 640 768ZM640 448C604.653776 448 576 476.653776 576 512 576 547.346224 604.653776 576 640 576 675.346221 576 704 547.346224 704 512 704 476.653776 675.346221 448 640 448ZM640 128C604.653776 128 576 156.653779 576 192 576 227.346221 604.653776 256 640 256 675.346221 256 704 227.346221 704 192 704 156.653779 675.346221 128 640 128Z"
              p-id="5664"
            ></path>
          </svg>
        </td>
        <td
          v-for="(col, colIndex) in columns"
          :key="colIndex"
          :style="columnStyles[col.dataIndex]"
          @click="onItemClick($event, item, index)"
        >
          <slot
            v-if="col.scopedSlots"
            :value="item[col.dataIndex]"
            :row="item"
            :column="col"
            :index="index"
            :name="col.scopedSlots.customRender"
          >
            <template v-if="col.type && col.type == 'checkbox'">
              <input
                type="checkbox"
                v-model="item.__selected"
                @click.stop="onItemChecked(item, index)"
              />
            </template>
            <template v-if="col.type && col.type == 'radio'">
              <input
                type="radio"
                :checked="item.__selected"
                @click.stop="onItemSelected(item, index)"
              />
            </template>
            <template v-else-if="col.type && col.type == 'number'">
              {{ index + 1 }}
            </template>
            <template v-else>
              {{
                col.customRender
                  ? col.customRender(item[col.dataIndex])
                  : item[col.dataIndex]
              }}
            </template>
          </slot>
          <template v-else>
            <template v-if="col.type && col.type == 'checkbox'">
              <input
                type="checkbox"
                v-model="item.__selected"
                @click.stop="onItemChecked(item, index)"
              />
            </template>
            <template v-if="col.type && col.type == 'radio'">
              <input
                type="radio"
                :checked="item.__selected"
                @click.stop="onItemSelected(item, index)"
              />
            </template>
            <template v-else-if="col.type && col.type == 'number'">
              {{ index + 1 }}
            </template>
            <template v-else>
              {{
                col.customRender
                  ? col.customRender(item[col.dataIndex])
                  : item[col.dataIndex]
              }}
            </template>
          </template>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from "vue";
import Sortable from "sortablejs";
import { message } from "ant-design-vue";

/**
 * 列配置类型DTO
 */
export type DropTableColumnConfig = {
  type?: "checkbox" | "radio" | "number";
  dataIndex: string;
  title: string;
  width?: number | string;
  align?: "left" | "center" | "right";
  customRender?(val: any): string;
  scopedSlots?: {
    customRender: string;
  };
};

export interface DropTableProps {
  dataSource?: Array<any>;
  columns: Array<DropTableColumnConfig>;
  keyField?: string;
  multiple?: boolean;
  disableDrag?: boolean;
}

const props = withDefaults(defineProps<DropTableProps>(), {
  dataSource: () => [],
  columns: () => [],
  keyField: "",
  multiple: false,
  disableDrag: false,
});

const emits = defineEmits(["selected", "checked", "dropsuccess"]);

const lastSelectedIndex = ref(0);
const columnStyles = ref<Record<string, any>>({});
const sortable = ref<any>(null);
const allChecked = ref(false);
const textRange: any = ref<any>(null);
const tableRef = ref<any>();

/**
 * 全选框事件处理
 * @param e
 */
const checkAll = (e: any) => {
  selectAll(e.target.checked);
};
/**
 * 获取选择行KEYS
 */
const getSelectedKeys = () => {
  return props.dataSource
    .filter((x) => x.__selected)
    .map((x) => x[props.keyField]);
};

/**
 * 获取选择行数据
 */
const getSelectedItems = () => {
  return props.dataSource.filter((x) => x.__selected);
};

/**
 * 设置选择行
 * @param keys
 */
const setSelectedKeys = (keys: Array<number | string>) => {
  props.dataSource.map((item) => {
    item.__selected = keys.indexOf(item[props.keyField]) !== -1;
  });
  let item = props.dataSource[0];
  props.dataSource.splice(0, 1, item);
  let items = getSelectedKeys();
  if (props.multiple) {
    emits("selected", items);
  } else {
    let index = items.length
      ? props.dataSource.findIndex((x) => x[props.keyField] == items[0])
      : -1;
    emits("selected", items[0], index);
  }
};

/**
 * 全选
 */
const selectAll = (checked?: boolean) => {
  props.dataSource.map((item) => {
    item.__selected = checked === false ? false : true;
  });
  let item = props.dataSource[0];
  props.dataSource.splice(0, 1, item);
  let items = getSelectedKeys();
  emits("selected", items);
};

/**
 * 反选
 */
const selectInverse = () => {
  props.dataSource.map((item) => {
    item.__selected = item.__selected ? false : true;
  });
  let item = props.dataSource[0];
  props.dataSource.splice(0, 1, item);
  let items = getSelectedKeys();
  emits("selected", items);
};

/**
 * 移动到最后
 * @param items
 */
const moveToLast = (items: Array<any>) => {
  if (!items || items.length == 0) {
    message.error("参数错误，要移动的行不存在");
    return;
  }
  let keys = items.map((x) => x[props.keyField]);
  let newItems: Array<any> = props.dataSource.filter(
    (x) => keys.indexOf(x[props.keyField]) === -1
  );
  newItems.push(...items);
  props.dataSource.splice(0, props.dataSource.length, ...newItems);
  scrollToRow(items[0]);
};

/**
 * 移动到最前
 * @param items
 */
const moveToFirst = (items: Array<any>) => {
  if (!items || items.length == 0) {
    message.error("参数错误，要移动的行不存在");
    return;
  }
  let keys = items.map((x) => x[props.keyField]);
  let newItems: Array<any> = props.dataSource.filter(
    (x) => keys.indexOf(x[props.keyField]) === -1
  );
  newItems.splice(0, 0, ...items);
  props.dataSource.splice(0, props.dataSource.length, ...newItems);
  scrollToIndex(0);
};

/**
 * 移动到指定行
 * @param targetRow 目标行
 * @param insertMode 插入方式 ：0 - 插入指定行之前 1 - 插入指定行之后
 * @param items
 */
const moveToRow = (targetRow: any, insertMode: number, items: Array<any>) => {
  if (!targetRow) {
    message.error("参数错误，要移动的目标位置不存在");
    return;
  }
  if (!items || items.length == 0) {
    message.error("参数错误，要移动的行不存在");
    return;
  }
  if (
    items.findIndex((x) => x[props.keyField] === targetRow[props.keyField]) !==
    -1
  ) {
    message.error("参数错误，要移动的目标行不允许为当前要移动的行之一");
    return;
  }
  let keys = items.map((x) => x[props.keyField]);
  let newItems: Array<any> = props.dataSource.filter(
    (x) => keys.indexOf(x[props.keyField]) === -1
  );
  let targetIndex = newItems.findIndex(
    (x) => x[props.keyField] == targetRow[props.keyField]
  );
  if (insertMode == 1) {
    targetIndex += 1;
  }
  newItems.splice(targetIndex, 0, ...items);
  props.dataSource.splice(0, props.dataSource.length, ...newItems);
  scrollToRow(items[0]);
};

/**
 * 移动到指定行索引
 * @param targetIndex
 * @param insertMode
 * @param items
 */
const moveToIndex = (
  targetIndex: number,
  insertMode: number,
  items: Array<any>
) => {
  let row = props.dataSource[targetIndex];
  moveToRow(row, insertMode, items);
};

const onTableClick = () => {
  let table = tableRef.value as HTMLTableElement;
  let tbody = table.querySelector("tbody");
  let rows = tbody?.querySelectorAll("tr");
  if (rows) {
    rows.forEach((item) => {
      item.className = (item.className || "").replace("find-row", "");
    });
  }
};

const findString = (str: string) => {
  if (parseInt(navigator.appVersion) < 4) {
    return false;
  }
  let self: any = window as any;
  let strFound;
  if ((window as any).find) {
    // CODE FOR BROWSERS THAT SUPPORT window.find
    strFound = self.find(str);
    if (strFound && self.getSelection && self.getSelection().anchorNode) {
      strFound = self.find(str);
      let scrollStr: any = self.getSelection().anchorNode;
      let scrollTop: number = scrollStr.parentNode.offsetTop;
      let table = tableRef.value as HTMLTableElement;
      let tbody = table.querySelector("tbody");
      let rows = tbody?.querySelectorAll("tr");
      if (rows) {
        rows.forEach((item) => {
          item.className = (item.className || "").replace("find-row", "");
        });
      }
      let tr = scrollStr.parentNode?.parentNode;
      if (tr) {
        tr.className = "find-row";
      }
      if (tbody) {
        tbody.scrollTop = scrollTop - 46;
      }
    }
    if (!strFound) {
      strFound = self.find(str, 0, 1);
      while (self.find(str, 0, 1)) {
        continue;
      }
    }
  } else if (navigator.appName.indexOf("Microsoft") != -1) {
    // EXPLORER-SPECIFIC CODE
    if (textRange.value != null) {
      textRange.value.collapse(false);
      strFound = textRange.value.findText(str);
      if (strFound) {
        textRange.value.select();
        return true;
      }
    }
    if (textRange.value == null || strFound == 0) {
      textRange.value = self.document.body.createTextRange();
      strFound = textRange.value.findText(str);
      if (strFound) {
        textRange.value.select();
        return true;
      }
    }
    return false;
  } else if (navigator.appName == "Opera") {
    message.error("很抱歉，该浏览器不支持搜索");
    return false;
  }
  if (!strFound) {
    message.error("搜索的关键词：" + str + "没有找到");
  }
  return false;
};

/**
 * 生成列配置
 */
watch(
  () => props.columns,
  (value: Array<any>) => {
    columnStyles.value = {};
    props.columns.map((col: any) => {
      let style: any = {
        textAlign: col.align || "left",
      };
      if (col.width) {
        if (typeof col.width === "number") {
          style.width = col.width + "px";
        } else {
          style.width = col.width;
        }
        style.flex = "none";
      } else {
        style.flex = 1;
      }
      columnStyles.value[col.dataIndex] = style;
    });
  },
  { immediate: true }
);

const onItemClick = (e: MouseEvent, item: any, rowIndex: number) => {
  e.stopPropagation();
  e.preventDefault();
  if (props.multiple) {
    if (e.shiftKey) {
      onShiftSelected(item, rowIndex);
    } else {
      onItemChecked(item, rowIndex);
    }
  } else {
    onItemSelected(item, rowIndex);
  }
};

/**
 * 选择行事件（多选）
 * @param item
 */
const onItemChecked = (item: any, rowIndex: number) => {
  item.__selected = item.__selected ? false : true;
  props.dataSource.splice(rowIndex, 1, item);
  let items = getSelectedKeys();
  emits("checked", items);
  emits("selected", items);
  if (item.__selected) {
    lastSelectedIndex.value = rowIndex;
  } else {
    lastSelectedIndex.value = -1;
  }
};

/**
 * 选择行事件（单选）
 * @param item
 * @param rowIndex
 */
const onItemSelected = (item: any, rowIndex: number) => {
  props.dataSource.map((row: any, index) => {
    row.__selected = rowIndex === index;
  });
  props.dataSource.splice(rowIndex, 1, item);
  let items = getSelectedKeys();
  emits("checked", items);
  emits("selected", item, rowIndex);
};

/**
 *
 */
const onShiftSelected = (item: any, index: number) => {
  let lastIndex = lastSelectedIndex.value;
  if (lastIndex == -1) {
    item.selected = true;
  } else {
    let startIndex = index > lastIndex ? lastIndex : index;
    let endIndex = index < lastIndex ? lastIndex : index;
    props.dataSource.map((row, rowIndex) => {
      row.__selected = rowIndex >= startIndex && rowIndex <= endIndex;
    });
  }
  if (lastIndex == -1) {
    lastIndex = index;
  }
  let items = getSelectedKeys();
  emits("checked", items);
  emits("selected", item, index);
};

/**
 * 滚动到指定行
 * @param row
 */
const scrollToRow = (row: any) => {
  let key = row[props.keyField];
  let table = tableRef.value as HTMLTableElement;
  let tbody = table.querySelector("tbody");
  let el: any = table.querySelector(`tr[data-key='${key}']`);
  if (el && tbody) {
    let scrollTop: number = el.offsetTop;
    tbody.scrollTop = scrollTop - 46;
  }
};

/**
 * 滚动到指定行 (行搜索)
 * @param rowNumber
 */
const scrollToIndex = (rowNumber: number) => {
  let row = props.dataSource[rowNumber];
  if (row) {
    scrollToRow(row);
  }
};

/**
 * 设置拖拽
 */
const initDragTable = () => {
  // 获取元素
  let table = tableRef.value as HTMLTableElement;
  const el = table.querySelector(".drop-table-body");
  sortable.value = Sortable.create(el as any, {
    handle: ".sortable-handle",
    ghostClass: "sortable-ghost",
    dragClass: "sortable-drag",
    animation: 0,
    setData: function (dataTransfer: any) {
      dataTransfer.setData("Text", "");
    },
    // 拖动结束时回调
    onEnd: (evt: any) => {
      const targetRow = props.dataSource.splice(evt.oldIndex, 1)[0];
      // 更新到表格数据
      props.dataSource.splice(evt.newIndex, 0, targetRow);
      emits("dropsuccess", {
        targetRow: targetRow,
        oldIndex: evt.oldIndex,
        newIndex: evt.newIndex,
      });
    },
  });
};

onMounted(() => {
  initDragTable();
});
</script>
<style lang="less" scoped>
@primary-color: #0081cc;

.drop-table {
  flex: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  border: 1px solid #ddd;
  user-select: text;
  .col-handle {
    width: 24px;
    flex: none;
    text-align: center;
  }

  &-head {
    flex: none;
    tr {
      min-width: 100%;
      display: flex;
    }
    th {
      padding: 12px 8px;
      overflow-wrap: break-word;
      color: rgba(0, 0, 0, 0.85);
      font-weight: 500;
      text-align: left;
      background: #fafafa;
      border-bottom: 1px solid #e8e8e8;
      transition: background 0.3s ease;
    }
  }
  &-body-empty {
    padding-top: 50px;
  }

  &-body,
  &-body-empty {
    flex: 1;
    overflow-y: auto;
    padding-bottom: 30px;
    background: #fcfcfc;
    display: block;
    tr {
      min-width: 100%;
      display: flex;
      background: #fff;
      &.selected {
        background: fade(@primary-color, 65%);
        color: #fff;
        td,
        th {
          color: #fff;
        }
      }

      &.find-row {
        background: rgba(255, 146, 36, 0.5);
        color: #fff;
        td,
        th {
          color: #fff;
        }
      }
    }

    td,
    th {
      padding: 12px 8px;
      border-bottom: 1px solid #e8e8e8;
      cursor: pointer;
      line-height: 150%;
      font-size: 14px;
      color: rgba(0, 0, 0, 0.85);
      font-weight: 400;
    }
  }
}

.sortable {
  &-ghost {
    background: rgba(255, 192, 0, 0.15) !important;
    color: #fff;
  }

  &-handle {
    cursor: move;
    font-size: 16px;
    fill: #888 !important;
  }

  &-drag {
    background: #01a058 !important;
    > td {
      color: #fff;
    }
    border: 1px solid #ffcc00;
  }
}
.td-span {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  -o-text-overflow: ellipsis;
}
</style>
